package org.apache.maven.wagon.providers.http;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.methods.multipart.PartSource;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateParser;
import org.apache.maven.wagon.AbstractWagon;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferEvent;
import org.apache.maven.wagon.resource.Resource;
import org.apache.maven.wagon.shared.http.HtmlFileListParser;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.GZIPInputStream;

/**
 * GoogleCodeHttpWagon
 *
 * @author <a href="cromwellian@gmail.com">Ray Cromwell</a>
 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="gcode" instantiation-strategy="per-lookup"
 */
public class GoogleCodeHttpWagon extends AbstractWagon {

  private static final int SC_NULL = -1;

  private HttpClient client;

  private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT");

  private HttpConnectionManager connectionManager;

  public void openConnection() {
   // SimpleHttpConnectionManager hm=new SimpleHttpConnectionManager();
   
    
    client = new HttpClient();

    String username = null;

    String password = null;

    if (authenticationInfo != null) {
      username = authenticationInfo.getUserName();

      password = authenticationInfo.getPassword();
    }

    String host = getRepository().getHost();
    if (true) {
      client.getParams().setAuthenticationPreemptive(true);
      Credentials creds = new UsernamePasswordCredentials(username, password);
      client.getState().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM), creds);
    }

    HostConfiguration hc = new HostConfiguration();

    if (proxyInfo != null) {
      String proxyUsername = proxyInfo.getUserName();

      String proxyPassword = proxyInfo.getPassword();

      String proxyHost = proxyInfo.getHost();

      int proxyPort = proxyInfo.getPort();

      String proxyNtlmHost = proxyInfo.getNtlmHost();

      String proxyNtlmDomain = proxyInfo.getNtlmDomain();

      if (proxyHost != null) {
        hc.setProxy(proxyHost, proxyPort);

        if (proxyUsername != null && proxyPassword != null) {
          Credentials creds;
          if (proxyNtlmHost != null || proxyNtlmDomain != null) {
            creds = new NTCredentials(proxyUsername, proxyPassword,
                proxyNtlmHost, proxyNtlmDomain);
          } else {
            creds = new UsernamePasswordCredentials(proxyUsername,
                proxyPassword);
          }

          client.getState().setProxyCredentials(null, proxyHost, creds);
          client.getState().setAuthenticationPreemptive(true);
        }
      }
    }

    hc.setHost(host);

    //start a session with the webserver
    client.setHostConfiguration(hc);
  }

  // put
  public void put(final File source, String resourceName) throws
      TransferFailedException, ResourceDoesNotExistException,
      AuthorizationException {
    String url = getUploadURL();

    Resource resource = new Resource(resourceName);

    firePutInitiated(resource, source);

    PostMethod postMethod = new PostMethod(url);
    postMethod.getParams().setSoTimeout(getTimeout());
    String fileName = resource.getName();
    int lind = fileName.lastIndexOf('/');
    fileName = fileName.substring(lind + 1);
  

    try {
      InputStream is=null;
      try {
         is = new PostInputStream(source, resource, this,
            getTransferEventSupport());
        postMethod.setRequestBody(is);
      } catch (FileNotFoundException e) {
        fireTransferError(resource, e, TransferEvent.REQUEST_PUT);

        throw new ResourceDoesNotExistException(
            "Source file does not exist: " + source, e);
      }

      final InputStream is1 = is;
      Part[] parts = {new StringPart("summary", "test"),
          new FilePart(fileName, new PartSource() {

            public long getLength() {
              return source.length();
            }

            public String getFileName() {
              return source.getName();
            }

            public InputStream createInputStream() throws IOException {
              return is1;
            }
          })};
      postMethod.setRequestEntity(
          new MultipartRequestEntity(parts, postMethod.getParams()));

      int statusCode = execute(postMethod);

      fireTransferDebug(url + " - Status code: " + statusCode);

      // Check that we didn't run out of retries.
      switch (statusCode) {
        // Success Codes
        case HttpStatus.SC_OK: // 200
        case HttpStatus.SC_CREATED: // 201
        case HttpStatus.SC_ACCEPTED: // 202
        case HttpStatus.SC_NO_CONTENT:  // 204
          break;

        case SC_NULL:
          throw new TransferFailedException("Failed to transfer file: " + url);

        case HttpStatus.SC_FORBIDDEN:
          throw new AuthorizationException("Access denied to: " + url);

        case HttpStatus.SC_NOT_FOUND:
          throw new ResourceDoesNotExistException(
              "File: " + url + " does not exist");

          //add more entries here
        default:
          throw new TransferFailedException("Failed to transfer file: " + url
              + ". Return code is: " + statusCode);
      }

      postMethod.releaseConnection();

      firePutCompleted(resource, source);
    } catch (Exception e) {
      e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
    }
  }

  public void closeConnection() {
  }

  public void get(String resourceName, File destination) throws
      TransferFailedException, ResourceDoesNotExistException,
      AuthorizationException {
    get(resourceName, destination, 0);
  }

  public boolean getIfNewer(String resourceName, File destination,
      long timestamp) throws TransferFailedException,
      ResourceDoesNotExistException, AuthorizationException {
    return get(resourceName, destination, timestamp);
  }

  /**
   * @param timestamp the timestamp to check against, only downloading if newer.
   *                  If <code>0</code>, always download
   * @return <code>true</code> if newer version was downloaded,
   *         <code>false</code> otherwise.
   */
  public boolean get(String resourceName, File destination, long timestamp)
      throws TransferFailedException, ResourceDoesNotExistException,
      AuthorizationException {
    Resource resource = new Resource(resourceName);

    fireGetInitiated(resource, destination);

    boolean retValue = false;

    String url = getUploadURL() + "/" + resourceName;

    GetMethod getMethod = new GetMethod(url);
    getMethod.getParams().setSoTimeout(getTimeout());

    try {
      // TODO: make these configurable

      getMethod.addRequestHeader("Cache-control", "no-cache");
      getMethod.addRequestHeader("Cache-store", "no-store");
      getMethod.addRequestHeader("Pragma", "no-cache");
      getMethod.addRequestHeader("Expires", "0");
      getMethod.addRequestHeader("Accept-Encoding", "gzip");

      if (timestamp > 0) {
        SimpleDateFormat fmt = new SimpleDateFormat(
            "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US);
        fmt.setTimeZone(GMT_TIME_ZONE);
        Header hdr = new Header("If-Modified-Since",
            fmt.format(new Date(timestamp)));
        fireTransferDebug("sending ==> " + hdr + "(" + timestamp + ")");
        getMethod.addRequestHeader(hdr);
      }

      int statusCode = execute(getMethod);

      fireTransferDebug(url + " - Status code: " + statusCode);

      // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is required
      switch (statusCode) {
        case HttpStatus.SC_OK:
          break;

        case HttpStatus.SC_NOT_MODIFIED:
          return false;

        case SC_NULL:
          throw new TransferFailedException("Failed to transfer file: " + url);

        case HttpStatus.SC_FORBIDDEN:
          throw new AuthorizationException("Access denied to: " + url);

        case HttpStatus.SC_UNAUTHORIZED:
          throw new AuthorizationException("Not authorized.");

        case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
          throw new AuthorizationException("Not authorized by proxy.");

        case HttpStatus.SC_NOT_FOUND:
          throw new ResourceDoesNotExistException(
              "File: " + url + " does not exist");

          //add more entries here
        default:
          throw new TransferFailedException("Failed to transfer file: " + url
              + ". Return code is: " + statusCode);
      }

      InputStream is = null;

      Header contentLengthHeader = getMethod
          .getResponseHeader("Content-Length");

      if (contentLengthHeader != null) {
        try {
          long contentLength = Integer.valueOf(contentLengthHeader.getValue())
              .intValue();

          resource.setContentLength(contentLength);
        } catch (NumberFormatException e) {
          fireTransferDebug("error parsing content length header '"
              + contentLengthHeader.getValue() + "' " + e);
        }
      }

      Header lastModifiedHeader = getMethod.getResponseHeader("Last-Modified");

      long lastModified = 0;

      if (lastModifiedHeader != null) {
        try {
          lastModified = DateParser.parseDate(lastModifiedHeader.getValue())
              .getTime();
        } catch (DateParseException e) {
          fireTransferDebug("Unable to parse last modified header");
        }

        fireTransferDebug("last-modified = " + lastModifiedHeader.getValue()
            + " (" + lastModified + ")");
      }

      // always get if timestamp is 0 (ie, target doesn't exist), otherwise only if older than the remote file
      if (timestamp == 0 || timestamp < lastModified) {
        retValue = true;

        Header contentEncoding = getMethod
            .getResponseHeader("Content-Encoding");
        boolean isGZipped = contentEncoding == null ? false
            : "gzip".equalsIgnoreCase(contentEncoding.getValue());

        try {
          is = getMethod.getResponseBodyAsStream();
          if (isGZipped) {
            is = new GZIPInputStream(is);
          }

          getTransfer(resource, destination, is);
        } catch (IOException e) {
          fireTransferError(resource, e, TransferEvent.REQUEST_GET);

          if (destination.exists()) {
            boolean deleted = destination.delete();

            if (!deleted) {
              destination.deleteOnExit();
            }
          }

          String msg = "Error occurred while deploying to remote repository:"
              + getRepository();

          throw new TransferFailedException(msg, e);
        } finally {
          try {
            is.close();
          } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
          }
        }

        if (lastModified > 0) {
          resource.setLastModified(lastModified);
        }
      } else {
        fireTransferDebug("Local file is newer: not downloaded");
      }

      return retValue;
    } finally {
      getMethod.releaseConnection();
    }
  }

  public List getFileList(String destinationDirectory) throws
      TransferFailedException, ResourceDoesNotExistException,
      AuthorizationException {
    if (!destinationDirectory.endsWith("/")) {
      destinationDirectory += "/";
    }

    String url = getRepository().getUrl() + "/" + destinationDirectory;

    GetMethod getMethod = new GetMethod(url);
    getMethod.getParams().setSoTimeout(getTimeout());

    try {
      // TODO: make these configurable

      getMethod.addRequestHeader("Cache-control", "no-cache");
      getMethod.addRequestHeader("Cache-store", "no-store");
      getMethod.addRequestHeader("Pragma", "no-cache");
      getMethod.addRequestHeader("Expires", "0");

      int statusCode = execute(getMethod);

      fireTransferDebug(url + " - Status code: " + statusCode);

      // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is required
      switch (statusCode) {
        case HttpStatus.SC_OK:
          break;

        case SC_NULL:
          throw new TransferFailedException("Failed to transfer file: ");

        case HttpStatus.SC_FORBIDDEN:
          throw new AuthorizationException("Access denied to: " + url);

        case HttpStatus.SC_UNAUTHORIZED:
          throw new AuthorizationException("Not authorized.");

        case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
          throw new AuthorizationException("Not authorized by proxy.");

        case HttpStatus.SC_NOT_FOUND:
          throw new ResourceDoesNotExistException(
              "File: " + url + " does not exist");

          //add more entries here
        default:
          throw new TransferFailedException("Failed to transfer file: " + url
              + ". Return code is: " + statusCode);
      }

      InputStream is = null;

      is = getMethod.getResponseBodyAsStream();

      return HtmlFileListParser.parseFileList(url, is);
    } catch (IOException e) {
      throw new TransferFailedException("Could not read response body.", e);
    } finally {
      getMethod.releaseConnection();
    }
  }

  private int getTimeout() {
    return 1000;
  }

  public boolean resourceExists(String resourceName)
      throws TransferFailedException, AuthorizationException {
    String url = getRepository().getUrl() + "/" + resourceName;

    HeadMethod headMethod = new HeadMethod(url);
    headMethod.getParams().setSoTimeout(getTimeout());

    int statusCode = execute(headMethod);

    try {
      switch (statusCode) {
        case HttpStatus.SC_OK:
          return true;

        case HttpStatus.SC_NOT_MODIFIED:
          return true;

        case SC_NULL:
          throw new TransferFailedException("Failed to transfer file: " + url);

        case HttpStatus.SC_FORBIDDEN:
          throw new AuthorizationException("Access denied to: " + url);

        case HttpStatus.SC_UNAUTHORIZED:
          throw new AuthorizationException("Not authorized.");

        case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
          throw new AuthorizationException("Not authorized by proxy.");

        case HttpStatus.SC_NOT_FOUND:
          return false;

          //add more entries here
        default:
          throw new TransferFailedException("Failed to transfer file: " + url
              + ". Return code is: " + statusCode);
      }
    } finally {
      headMethod.releaseConnection();
    }
  }

  private int execute(HttpMethod httpMethod) throws TransferFailedException {
    int statusCode = SC_NULL;
    try {
      // execute the method.
      statusCode = client.executeMethod(httpMethod);
    } catch (IOException e) {
      throw new TransferFailedException(e.getMessage(), e);
    }
    return statusCode;
  }

  public void setConnectionManager(HttpConnectionManager connectionManager) {
    this.connectionManager = connectionManager;
  }

  final String BOUNDARY = "----------Googlecode_boundary_reindeer_flotilla";

  private String getUploadURL() {
    String repositoryUrl = getRepository().getUrl();
    System.err.println("Repo URL is " + repositoryUrl);
    if (repositoryUrl.startsWith("gcode:")) {
      repositoryUrl = repositoryUrl.substring("gcode:".length());
    }
    return repositoryUrl;
  }

  private URL resolveResourceURL(Resource resource)
      throws MalformedURLException {
    String repositoryUrl = getUploadURL();

    URL url;
    if (repositoryUrl.endsWith("/")) {
      url = new URL(repositoryUrl + resource.getName());
    } else {
      url = new URL(repositoryUrl + "/" + resource.getName());
    }
    return url;
  }

  // The following code lifted from the Ant GoogleCode project at
  // http://code.google.com/p/ant-googlecode

  /**
   * Just sends an ASCII version of the given string, followed by a CRLF line
   * terminator, to the given output stream.
   */
  private void sendLine(OutputStream out, String string) throws IOException {
    System.err.write(string.getBytes("ascii"));
    System.err.write("\r\n".getBytes("ascii"));
    out.write(string.getBytes("ascii"));
    out.write("\r\n".getBytes("ascii"));
  }

  /**
   * Creates a (base64-encoded) HTTP basic authentication token for the given
   * user name and password.
   */
  private static String createAuthToken(String userName, String password) {
    Base64Converter b64 = new Base64Converter();
    return b64.encode(userName + ":" + password);
  }

  protected static class Base64Converter {

    public final char[] alphabet = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
        //  0 to  7
        'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',  //  8 to 15
        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',  // 16 to 23
        'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',  // 24 to 31
        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',  // 32 to 39
        'o', 'p', 'q', 'r', 's', 't', 'u', 'v',  // 40 to 47
        'w', 'x', 'y', 'z', '0', '1', '2', '3',  // 48 to 55
        '4', '5', '6', '7', '8', '9', '+', '/'}; // 56 to 63

    public String encode(String s) {
      return encode(s.getBytes());
    }

    public String encode(byte[] octetString) {
      int bits24;
      int bits6;

      char[] out = new char[((octetString.length - 1) / 3 + 1) * 4];
      int outIndex = 0;
      int i = 0;

      while ((i + 3) <= octetString.length) {
        // store the octets
        bits24 = (octetString[i++] & 0xFF) << 16;
        bits24 |= (octetString[i++] & 0xFF) << 8;
        bits24 |= octetString[i++];

        bits6 = (bits24 & 0x00FC0000) >> 18;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x0003F000) >> 12;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x00000FC0) >> 6;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x0000003F);
        out[outIndex++] = alphabet[bits6];
      }
      if (octetString.length - i == 2) {
        // store the octets
        bits24 = (octetString[i] & 0xFF) << 16;
        bits24 |= (octetString[i + 1] & 0xFF) << 8;
        bits6 = (bits24 & 0x00FC0000) >> 18;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x0003F000) >> 12;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x00000FC0) >> 6;
        out[outIndex++] = alphabet[bits6];

        // padding
        out[outIndex++] = '=';
      } else if (octetString.length - i == 1) {
        // store the octets
        bits24 = (octetString[i] & 0xFF) << 16;
        bits6 = (bits24 & 0x00FC0000) >> 18;
        out[outIndex++] = alphabet[bits6];
        bits6 = (bits24 & 0x0003F000) >> 12;
        out[outIndex++] = alphabet[bits6];

        // padding
        out[outIndex++] = '=';
        out[outIndex++] = '=';
      }
      return new String(out);
    }
  }

  public interface DownloadProgress {

    /**
     * begin a download
     */
    public void beginDownload();

    /**
     * tick handler
     */
    public void onTick();

    /**
     * end a download
     */
    public void endDownload();
  }

  /**
   * do nothing with progress info
   */
  public static class NullProgress implements DownloadProgress {

    /**
     * begin a download
     */
    public void beginDownload() {

    }

    /**
     * tick handler
     */
    public void onTick() {
    }

    /**
     * end a download
     */
    public void endDownload() {

    }
  }

  /**
   * verbose progress system prints to some output stream
   */
  public static class VerboseProgress implements DownloadProgress {

    private int dots = 0;

    PrintStream out;

    public VerboseProgress(PrintStream out) {
      this.out = out;
    }

    /**
     * begin a download
     */
    public void beginDownload() {
      dots = 0;
    }

    /**
     * tick handler
     */
    public void onTick() {
      out.print(".");
      if (dots++ > 50) {
        out.flush();
        dots = 0;
      }
    }

    /**
     * end a download
     */
    public void endDownload() {
      out.println();
      out.flush();
    }
  }
}


